feat(ENG-13677): Terraform credential helper - #400
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a Terraform credentials helper integration to Cloudsmith CLI so terraform init can authenticate against Cloudsmith Terraform registries via the existing CLI credential chain (env/config/keyring/OIDC), plus installer logic to wire Terraform’s plugin dir and ~/.terraformrc.
Changes:
- Introduces Terraform helper runtime + terraformrc block editor + installer that writes a
terraform-credentials-cloudsmithlauncher into Terraform’s plugin directory and manages acredentials_helper "cloudsmith"block. - Adds CLI wiring for
cloudsmith credential-helper terraformand extendscredential-helper installto support Terraform-specific baked args (org/profile/repo) and next-steps guidance. - Adds unit + integration tests and refactors domain detection by introducing
is_standard_cloudsmith_domain().
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cloudsmith_cli/credential_helpers/terraform/terraformrc.py | Text-based add/update/remove of the credentials_helper "cloudsmith" terraformrc block. |
| cloudsmith_cli/credential_helpers/terraform/runtime.py | Implements Terraform credentials-helper protocol behavior and scoped token formatting. |
| cloudsmith_cli/credential_helpers/terraform/installer.py | Installs/removes the launcher into Terraform plugin dir and updates terraformrc. |
| cloudsmith_cli/credential_helpers/terraform/init.py | Exposes Terraform helper runtime functions at package level. |
| cloudsmith_cli/credential_helpers/common.py | Adds is_standard_cloudsmith_domain() and reuses it in is_cloudsmith_domain(). |
| cloudsmith_cli/cli/commands/credential_helper/terraform.py | Click command shim for Terraform helper (parses verb/hostname, prints JSON). |
| cloudsmith_cli/cli/commands/credential_helper/manage.py | Registers Terraform installer; adds Terraform repo bake + next-steps output. |
| cloudsmith_cli/cli/commands/credential_helper/init.py | Registers the terraform subcommand under credential-helper. |
| cloudsmith_cli/cli/tests/test_startup_imports.py | Adds import-safety tests for Terraform helper (and wrapper). |
| cloudsmith_cli/cli/tests/test_credential_helper_terraform.py | Adds runtime/CLI/wrapper behavior tests for Terraform helper. |
| cloudsmith_cli/cli/tests/test_credential_helper_terraform_installer.py | Adds terraformrc + installer + CLI install/uninstall tests. |
| cloudsmith_cli/cli/tests/commands/test_credential_helper.py | Adds unit coverage for is_standard_cloudsmith_domain(). |
| cloudsmith_cli/cli/tests/commands/test_credential_helper_terraform_integration.py | Adds live terraform init integration test exercising helper auth path. |
| CHANGELOG.md | Documents the new Terraform credentials helper feature. |
Suppressed comments (1)
cloudsmith_cli/cli/tests/test_startup_imports.py:76
- This test imports
cloudsmith_cli.credential_helpers.terraform.wrapper, but there is nowrapper.pyundercloudsmith_cli/credential_helpers/terraform/in this PR (only__init__.py,runtime.py,installer.py,terraformrc.py). This will fail at import time and break the suite.
Either add the missing wrapper module (and its packaging entry point if it’s meant to be an installed executable), or adjust/remove the wrapper-related tests if the launcher written by TerraformInstaller is the only wrapper being shipped.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- token returns expected owner/repo/token format - installer now takes --repo flag to set the reop - installer allows reinstalling/overwriting of repo
176f131 to
6766bbf
Compare
prevent bin-dir from being outside of what is allowed [here](https://developer.hashicorp.com/terraform/cli/config/config-file#implied-local-mirror-directories).
|
@BartoszBlizniak this is ready for another look now |
| See: | ||
| https://developer.hashicorp.com/terraform/cli/config/config-file#implied-local-mirror-directories | ||
| """ | ||
| dirs: list[Path] = [_default_plugin_dir()] |
There was a problem hiding this comment.
This allowlist is built from the "Implied Local Mirror Directories" doc, but that section describes where Terraform looks for provider mirrors, not credentials helpers. Helper discovery is much narrower in the Terraform source:
commands.go#L507-L509:FindPlugins("credentials", cliconfig.GlobalPluginDirs())cliconfig/plugins.go:GlobalPluginDirs()returns only<ConfigDir>/pluginsand<ConfigDir>/plugins/<GOOS>_<GOARCH>ConfigDir()is~/.terraform.don Unix and%APPDATA%\terraform.don Windows (config_unix.go,config_windows.go). No XDG handling.discovery/find.go#L54does a flatReadDiron each of those dirs. Not recursive.
So as written this still allows installs Terraform will never see:
- The XDG data dirs,
/usr/local/share//usr/share, the macOSio.terraformdirs and the cwdterraform.d/pluginsare never searched for helpers. Installing there "succeeds" andterraform initsilently ignores the helper, which is the failure mode this change was meant to prevent. _is_recognized_plugin_diraccepts any descendant of a root, but only<root>and<root>/<os>_<arch>are searched.~/.terraform.d/plugins/customis not found by Terraform.- Line 128 has
"HashCorp"(should beHashiCorp). Moot once the branch goes, but worth noting the list was not verified against a real Terraform.
Suggest reducing the allowlist to exactly _default_plugin_dir() and _default_plugin_dir() / f"{os}_{arch}", and dropping the XDG / macOS / cwd branches and their tests. Could you also verify with a real terraform init and TF_LOG=debug that a launcher in a rejected dir is in fact not picked up, and one in the accepted dirs is?
| """ | ||
| monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path)) | ||
| monkeypatch.delenv("TF_CLI_CONFIG_FILE", raising=False) | ||
| custom = tmp_path / ".terraform.d" / "plugins" / "custom" |
There was a problem hiding this comment.
This test asserts that installing into ~/.terraform.d/plugins/custom succeeds, but Terraform does a flat ReadDir on plugins/ and plugins/<os>_<arch> only, so a launcher here is never discovered. The test currently locks in the bug. Once the allowlist is tightened, the only valid subdirectory to use here is an <os>_<arch> one, e.g. linux_amd64.
Description
Adds a Terraform credentials helper for Cloudsmith registries so
terraform initcan authenticate against a Cloudsmith Terraform registry withno token on disk, reusing the existing Cloudsmith CLI credential chain
(
--api-key/CLOUDSMITH_API_KEY,credentials.ini, the OS keyring, or OIDC).Terraform discovers credentials helpers as executables named
terraform-credentials-<name>and only searches its plugin directories (never$PATH). This PR ships the helper, a launcher/wrapper that satisfies thatdiscovery contract, an installer that wires up
~/.terraformrc, and therepository-scoped token format the registry expects.
What's included
Command (
cloudsmith credential-helper terraform)getverb.Accepts Terraform's
[verb] <hostname>calling convention (verb defaults toget; hostname falls back to stdin) so the launcher can forward argsverbatim.
{"token": "..."}for a Cloudsmith host and{}for any other host(exit 0) so Terraform falls back to its own credential sources.
store/forgetand unknown verbs return an actionable error and a non-zeroexit. Missing credentials for a Cloudsmith host produce a clean refusal, never
a traceback.
credential-helpergroup.Installer (
credential-helper install/uninstall/list terraform)~/.terraform.d/pluginsbydefault;
--bin-diroverrides) and manages thecredentials_helper "cloudsmith"block in~/.terraformrcvia a regex block editor (no HCLparser); refuses if a different helper block already exists.
--org,-P/--profile, and--repointo the terraformrcargslist soterraform initneeds no environment variables. Computes theterraformrc change before writing the launcher so a conflict leaves no orphan.
Repository-scoped, custom-domain-aware token format
-r/--repo/--repository(orCLOUDSMITH_REPO) is required onget, andcan also be passed to
installto bake it into the terraformrcargs.*.cloudsmith.io/*.cloudsmith.comhosts →{org}/{repo}/{token}(the organisation is required here).
{repo}/{token}(the org is used only to resolve the domain, never emitted in the token).
is_standard_cloudsmith_domain()tocredential_helpers/common.pytodistinguish standard hosts from custom domains (
is_cloudsmith_domainwasrefactored to reuse it; behaviour unchanged).
Type of Change
Testing
get/store/forget, token / empty-object /refusal outcomes, org-required-on-standard-domain, org-omitted-on-custom-
domain), the CLI shim, the wrapper delegation, the terraformrc block
helpers, and the installer (launcher + terraformrc,
--org/-P/--repobaking, conflict handling).
is_standard_cloudsmith_domainunit coverage (apex/subdomain/scheme/casingmatches, plus custom-domain and lookalike non-matches).
@pytest.mark.integration) that runs a realterraform initin an isolatedHOMEand asserts it does not fail forauthentication reasons; skips cleanly when
terraform, the wrapper, or thePYTEST_CLOUDSMITH_*vars are absent.Additional Notes
terraformrc block management, the required repo flag, and the token format).